// 版权所有2011 Go作者。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

// Package get实现了“go get”命令。
package get

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"

	"cmd/go/internal/base"
	"cmd/go/internal/cfg"
	"cmd/go/internal/load"
	"cmd/go/internal/search"
	"cmd/go/internal/str"
	"cmd/go/internal/vcs"
	"cmd/go/internal/web"
	"cmd/go/internal/work"

	"golang.org/x/mod/module"
)

var CmdGet = &base.Command{
	UsageLine: "go get [-d] [-f] [-t] [-u] [-v] [-fix] [build flags] [packages]",
	Short:     "download and install packages and dependencies",
	Long: `
Get downloads the packages named by the import paths, along with their
dependencies. It then installs the named packages, like 'go install'.

The -d flag instructs get to stop after downloading the packages; that is,
it instructs get not to install the packages.

The -f flag, valid only when -u is set, forces get -u not to verify that
each package has been checked out from the source control repository
implied by its import path. This can be useful if the source is a local fork
of the original.

The -fix flag instructs get to run the fix tool on the downloaded packages
before resolving dependencies or building the code.

The -t flag instructs get to also download the packages required to build
the tests for the specified packages.

The -u flag instructs get to use the network to update the named packages
and their dependencies. By default, get uses the network to check out
missing packages but does not use it to look for updates to existing packages.

The -v flag enables verbose progress and debug output.

Get also accepts build flags to control the installation. See 'go help build'.

When checking out a new package, get creates the target directory
GOPATH/src/<import-path>. If the GOPATH contains multiple entries,
get uses the first one. For more details see: 'go help gopath'.

When checking out or updating a package, get looks for a branch or tag
that matches the locally installed version of Go. The most important
rule is that if the local installation is running version "go1", get
searches for a branch or tag named "go1". If no such version exists
it retrieves the default branch of the package.

When go get checks out or updates a Git repository,
it also updates any git submodules referenced by the repository.

Get never checks out or updates code stored in vendor directories.

For more about specifying packages, see 'go help packages'.

For more about how 'go get' finds source code to
download, see 'go help importpath'.

This text describes the behavior of get when using GOPATH
to manage source code and dependencies.
If instead the go command is running in module-aware mode,
the details of get's flags and effects change, as does 'go help get'.
See 'go help modules' and 'go help module-get'.

See also: go build, go install, go clean.
	`,
}

var HelpGopathGet = &base.Command{
	UsageLine: "gopath-get",
	Short:     "legacy GOPATH go get",
	Long: `
The 'go get' command changes behavior depending on whether the
go command is running in module-aware mode or legacy GOPATH mode.
This help text, accessible as 'go help gopath-get' even in module-aware mode,
describes 'go get' as it operates in legacy GOPATH mode.

Usage: ` + CmdGet.UsageLine + `
` + CmdGet.Long,
}

var (
	getD        = CmdGet.Flag.Bool("d", false, "")
	getF        = CmdGet.Flag.Bool("f", false, "")
	getT        = CmdGet.Flag.Bool("t", false, "")
	getU        = CmdGet.Flag.Bool("u", false, "")
	getFix      = CmdGet.Flag.Bool("fix", false, "")
	getInsecure = CmdGet.Flag.Bool("insecure", false, "")
)

func init() {
	work.AddBuildFlags(CmdGet, work.OmitModFlag|work.OmitModCommonFlags)
	CmdGet.Run = runGet // 中断初始化循环
}

func runGet(ctx context.Context, cmd *base.Command, args []string) {
	if cfg.ModulesEnabled {
		// 不应发生：main。go应该安装单独的模块启用get代码。cun chuang jian defg
		base.Fatalf("go: modules not implemented")
	}

	work.BuildInit()

	if *getF && !*getU {
		base.Fatalf("go: cannot use -f flag without -u")
	}
	if *getInsecure {
		base.Fatalf("go: -insecure flag is no longer supported; use GOINSECURE instead")
	}

	// 仅对2.3.0或更高版本有效，但避免使用
	// 在早期版本中的提示太难了。
	// 如果用户已显式设置GIT_TERMINAL_PROMPT=1，则保持
	// 提示。
	// 见戈朗。org/issue/9341和golang。org/issue/12706。
	if os.Getenv("GIT_TERMINAL_PROMPT") == "" {
		os.Setenv("GIT_TERMINAL_PROMPT", "0")
	}

	// 还禁用由
	// Git生成的“ssh”子进程对密码的提示，因为显然Git_TERMINAL_提示不足以做到这一点。
	// 添加'-o BatchMode=yes'应该可以。
	// 
	// 如果Git子进程将一个子进程转移到后台以缓存新连接，
	// 该子进程将保持stdout/stderr打开。Git子进程退出后，
	// os/exec希望能够从stdout/stderr管道读取
	// ，直到EOF获取Git子进程退出前写入的所有数据。
	// 在子进程退出之前，EOF不会出现，因为子进程
	// 正在控制管道的写入端。
	// 这是不幸的，但它至少出现了两次
	// （请参见golang.org/issue/13453和golang.org/issue/16104）
	// 并在出现时混淆了用户。
	// 如果用户明确设置了GIT_SSH或GIT_SSH_命令，则
	// 假设他们知道自己在做什么，并且没有踩到它。
	// 但默认关闭ControlMaster。
	if os.Getenv("GIT_SSH") == "" && os.Getenv("GIT_SSH_COMMAND") == "" {
		os.Setenv("GIT_SSH_COMMAND", "ssh -o ControlMaster=no -o BatchMode=yes")
	}

	// 还有另外一个Git提示源：Git Credential Manager Core for Windows。
	// 
	// 参见https:
	if os.Getenv("GCM_INTERACTIVE") == "" {
		os.Setenv("GCM_INTERACTIVE", "never")
	}

	// 第一阶段。下载/更新。
	var stk load.ImportStack
	mode := 0
	if *getT {
		mode |= load.GetTestDeps
	}
	for _, pkg := range downloadPaths(args) {
		download(pkg, nil, &stk, mode)
	}
	base.ExitIfErrors()

	// 第二阶段。重新扫描包并重新评估参数列表。

	// 我们下载的代码和所有依赖它的代码
	// 需要从包缓存中删除，以便重新计算信息。不要让
	// 跟踪反向依赖信息，而是逐出
	// 所有内容。第三阶段。安装
	load.ClearPackageCache()

	pkgs := load.PackagesAndErrors(ctx, load.PackageOpts{}, args)
	load.CheckPackageErrors(pkgs)

	if *getD {
		// 仅限下载。
		// 检查延迟到现在，以便下载路径
		// 和检查包错误有机会打印错误。
		return
	}

	work.InstallPackages(ctx, args, pkgs)
}

// downloadPaths准备要传递到下载的路径列表。
// 它扩展了。。。可以扩展的模式。如果没有匹配的
// 特定模式，DownloadPath会将其保留在结果列表中，
// 希望我们能从
// 初始-免费前缀。
func downloadPaths(patterns []string) []string {
	for _, arg := range patterns {
		if strings.Contains(arg, "@") {
			base.Fatalf("go: can only use path@version syntax with 'go get' and 'go install' in module-aware mode")
			continue
		}

		// 防止“go get x.go”这一常见错误。
		// 请注意，包和模块路径可能以“.”结尾。go'，因此，如果参数没有斜杠或引用现有文件，则只打印一个错误
		// 。
		if strings.HasSuffix(arg, ".go") {
			if !strings.Contains(arg, "/") {
				base.Errorf("go: %s: arguments must be package or module paths", arg)
				continue
			}
			if fi, err := os.Stat(arg); err == nil && !fi.IsDir() {
				base.Errorf("go: %s exists as a file, but 'go get' requires package arguments", arg)
			}
		}
	}
	base.ExitIfErrors()

	var pkgs []string
	noModRoots := []string{}
	for _, m := range search.ImportPathsQuiet(patterns, noModRoots) {
		if len(m.Pkgs) == 0 && strings.Contains(m.Pattern(), "...") {
			pkgs = append(pkgs, m.Pattern())
		} else {
			pkgs = append(pkgs, m.Pkgs...)
		}
	}
	return pkgs
}

// downloadCache记录我们在下载过程中已经考虑的导入路径
// ，以避免在
// 有多个依赖序列导致
// 特定包时重复工作。
var downloadCache = map[string]bool{}

// downloadRootCache记录版本控制存储库
// 我们在下载过程中已经考虑过的根目录。
// 例如，github中的所有包。COM/谷歌/CODESARCHE RPO ABCFDG
var downloadRootCache = map[string]bool{}

// download为参数命名的包或模式运行get命令
// 的下载部分。
func download(arg string, parent *load.Package, stk *load.ImportStack, mode int) {
	if mode&load.ResolveImport != 0 {
		// 调用方负责扩展供应商路径。
		panic("internal error: download mode has useVendor set")
	}
	load1 := func(path string, mode int) *load.Package {
		if parent == nil {
			mode := 0 // 不执行模块或供应商解析
			return load.LoadImport(context.TODO(), load.PackageOpts{}, path, base.Cwd(), nil, stk, nil, mode)
		}
		return load.LoadImport(context.TODO(), load.PackageOpts{}, path, parent.Dir, parent, stk, nil, mode|load.ResolveModule)
	}

	p := load1(arg, mode)
	if p.Error != nil && p.Error.Hard {
		base.Errorf("%s", p.Error)
		return
	}

	// loadPackage从arg推断出规范的导入路径。
	// 在以下情况下使用该选项以防止滞后效应
	// 例如在downloadCache和packageCache中。
	// 这允许调用，例如：
	// mkdir-p$GOPATH/src/github。com/user 
	// cd$GOPATH/src/github。com/user 
	// 去拿/foo 
	// 参见：golang。org/issue/9767 
	arg = p.ImportPath

	// 如果这是标准库中的一个包，则无需执行任何操作。
	if p.Standard {
		return
	}

	// 每个包只处理一次。
	// （除非我们获取此包的测试依赖项，否则
	// ，在这种情况下，我们希望再次处理它。）
	if downloadCache[arg] && mode&load.GetTestDeps == 0 {
		return
	}
	downloadCache[arg] = true

	pkgs := []*load.Package{p}
	wildcardOkay := len(*stk) == 0
	isWildcard := false

	// 如果软件包丢失，请下载，如果我们使用-u，请更新。
	if p.Dir == "" || *getU {
		// 实际下载。
		stk.Push(arg)
		err := downloadPackage(p)
		if err != nil {
			base.Errorf("%s", &load.PackageError{ImportStack: stk.Copy(), Err: err})
			stk.Pop()
			return
		}
		stk.Pop()

		args := []string{arg}
		// 如果参数中有通配符，请重新计算通配符。
		// 我们将此延迟到重新加载包之后，以便在包缓存中替换p的旧条目
		// 。
		if wildcardOkay && strings.Contains(arg, "...") {
			match := search.NewMatch(arg)
			if match.IsLocal() {
				noModRoots := []string{} // 我们处于gopath模式，因此没有modroots。
				match.MatchDirs(noModRoots)
				args = match.Dirs
			} else {
				match.MatchPackages()
				args = match.Pkgs
			}
			for _, err := range match.Errs {
				base.Errorf("%s", err)
			}
			isWildcard = true
		}

		// 在执行任何新加载之前，请清除所有相关的包缓存项。
		load.ClearPackageCachePartial(args)

		pkgs = pkgs[:0]
		for _, arg := range args {
			// 注意：load调用loadPackage或loadImport，
			// 这已经将arg推到stk上了。
			// 不要推到这里，否则stk会说arg导入arg。
			p := load1(arg, mode)
			if p.Error != nil {
				base.Errorf("%s", p.Error)
				continue
			}
			pkgs = append(pkgs, p)
		}
	}

	// 进程包，由于通配符扩展，它现在可能是多个包。
	for _, p := range pkgs {
		if *getFix {
			files := base.RelPaths(p.InternalAllGoFiles())
			base.Run(cfg.BuildToolexec, str.StringList(base.Tool("fix"), files))

			// 导入可能已更改，请重新加载。
			p = load.ReloadPackageNoFlags(arg, stk)
			if p.Error != nil {
				base.Errorf("%s", p.Error)
				return
			}
		}

		if isWildcard {
			// 在任何错误消息中报告真实的包和
			// 通配符。
			stk.Push(p.ImportPath)
		}

		// 现在我们知道了流程依赖关系是什么。
		imports := p.Imports
		if mode&load.GetTestDeps != 0 {
			// 指定-t时的流程测试依赖项。
			// （但不要获取测试依赖项的测试依赖项：
			// 我们总是将模式0传递给下面的递归调用。）
			imports = str.StringList(imports, p.TestImports, p.XTestImports)
		}
		for i, path := range imports {
			if path == "C" {
				continue
			}
			// 导入命名完整供应商路径时快速失败。
			// 否则，请根据测试导入的需要扩展路径。
			// 请注意，p.Imports可以在p.Internal之外有其他条目。建筑进口。
			orig := path
			if i < len(p.Internal.Build.Imports) {
				orig = p.Internal.Build.Imports[i]
			}
			if j, ok := load.FindVendor(orig); ok {
				stk.Push(path)
				err := &load.PackageError{
					ImportStack: stk.Copy(),
					Err:         load.ImportErrorf(path, "%s must be imported as %s", path, path[j+len("vendor/"):]),
				}
				stk.Pop()
				base.Errorf("%s", err)
				continue
			}
			// 如果这是测试导入，请立即应用模块和供应商查找。
			// 我们无法将ResolveImport传递给下载，因为
			// 下载基于path的值进行缓存，所以它必须是完全限定的路径。
			if i >= len(p.Imports) {
				path = load.ResolveImportPath(p, path)
			}
			download(path, p, stk, 0)
		}

		if isWildcard {
			stk.Pop()
		}
	}
}

// downloadPackage运行create或download命令
// 创建给定包的第一个副本或更新给定包的副本。
func downloadPackage(p *load.Package) error {
	var (
		vcsCmd                  *vcs.Cmd
		repo, rootPath, repoDir string
		err                     error
		blindRepo               bool // 设置回购是否有异常配置
	)

	// p可以是真实的包，也可以是“导入路径”为
	// 实际上是通配符模式的伪包。
	// 在包含第一个通配符的元素处修剪路径，
	// 希望它也适用于通配符部分。
	// 这使得“去拿rsc”。io/pdf/…'在一条新的道路上工作。
	importPrefix := p.ImportPath
	if i := strings.Index(importPrefix, "..."); i >= 0 {
		slash := strings.LastIndexByte(importPrefix[:i], '/')
		if slash < 0 {
			return fmt.Errorf("cannot expand ... in %q", p.ImportPath)
		}
		importPrefix = importPrefix[:slash]
	}
	if err := checkImportPath(importPrefix); err != nil {
		return fmt.Errorf("%s: invalid import path: %v", p.ImportPath, err)
	}
	security := web.SecureOnly
	if module.MatchPrefixPatterns(cfg.GOINSECURE, importPrefix) {
		security = web.Insecure
	}

	if p.Internal.Build.SrcRoot != "" {
		// 目录已存在。沿着通往src的路径寻找结帐。
		const allowNesting = false
		repoDir, vcsCmd, err = vcs.FromDir(p.Dir, p.Internal.Build.SrcRoot, allowNesting)
		if err != nil {
			return err
		}
		if !str.HasFilePathPrefix(repoDir, p.Internal.Build.SrcRoot) {
			panic(fmt.Sprintf("repository %q not in source root %q", repo, p.Internal.Build.SrcRoot))
		}
		rootPath = str.TrimFilePathPrefix(repoDir, p.Internal.Build.SrcRoot)
		if err := vcs.CheckGOVCS(vcsCmd, rootPath); err != nil {
			return err
		}

		repo = "<local>" // 应该不用；制作独特的

		// 仔细检查它的来源。
		if *getU && vcsCmd.RemoteRepo != nil {
			dir := filepath.Join(p.Internal.Build.SrcRoot, filepath.FromSlash(rootPath))
			remote, err := vcsCmd.RemoteRepo(vcsCmd, dir)
			if err != nil {
				// 还是继续吧。包裹存在；我们可能只是不理解
				// 回购配置（例如异常远程协议）。
				blindRepo = true
			}
			repo = remote
			if !*getF && err == nil {
				if rr, err := vcs.RepoRootForImportPath(importPrefix, vcs.IgnoreMod, security); err == nil {
					repo := rr.Repo
					if rr.VCS.ResolveRepo != nil {
						resolved, err := rr.VCS.ResolveRepo(rr.VCS, dir, repo)
						if err == nil {
							repo = resolved
						}
					}
					if remote != repo && rr.IsCustom {
						return fmt.Errorf("%s is a custom import path for %s, but %s is checked out from %s", rr.Root, repo, dir, remote)
					}
				}
			}
		}
	} else {
		// 分析导入路径以确定版本控制系统、
		// 存储库以及存储库根目录的导入路径。
		rr, err := vcs.RepoRootForImportPath(importPrefix, vcs.IgnoreMod, security)
		if err != nil {
			return err
		}
		vcsCmd, repo, rootPath = rr.VCS, rr.Repo, rr.Root
	}
	if !blindRepo && !vcsCmd.IsSecure(repo) && security != web.Insecure {
		return fmt.Errorf("cannot download, %v uses insecure protocol", repo)
	}

	if p.Internal.Build.SrcRoot == "" {
		// 找不到包。放入$GOPATH的第一个目录。
		list := filepath.SplitList(cfg.BuildContext.GOPATH)
		if len(list) == 0 {
			return fmt.Errorf("cannot download, $GOPATH not set. For more details see: 'go help gopath'")
		}
		// 防止设置GOPATH=$GOROOT的人。
		if filepath.Clean(list[0]) == filepath.Clean(cfg.GOROOT) {
			return fmt.Errorf("cannot download, $GOPATH must not be set to $GOROOT. For more details see: 'go help gopath'")
		}
		if _, err := os.Stat(filepath.Join(list[0], "src/cmd/go/alldocs.go")); err == nil {
			return fmt.Errorf("cannot download, %s is a GOROOT, not a GOPATH. For more details see: 'go help gopath'", list[0])
		}
		p.Internal.Build.Root = list[0]
		p.Internal.Build.SrcRoot = filepath.Join(list[0], "src")
		p.Internal.Build.PkgRoot = filepath.Join(list[0], "pkg")
	}
	root := filepath.Join(p.Internal.Build.SrcRoot, filepath.FromSlash(rootPath))

	if err := vcs.CheckNested(vcsCmd, root, p.Internal.Build.SrcRoot); err != nil {
		return err
	}

	// 如果我们已经考虑过这个存储库，就不要再这样做了。
	if downloadRootCache[root] {
		return nil
	}
	downloadRootCache[root] = true

	if cfg.BuildV {
		fmt.Fprintf(os.Stderr, "%s (download)\n", rootPath)
	}

	// 检查此处是否适合检查回购协议。
	// 目标目录必须不存在或已签出回购。
	meta := filepath.Join(root, "."+vcsCmd.Cmd)
	if _, err := os.Stat(meta); err != nil {
		// 元数据文件或目录不存在。准备签出新副本。
		// 某些版本控制工具要求目标目录不存在。
		// 我们也要求这样做，只是为了避免妨碍现有的工作。
		if _, err := os.Stat(root); err == nil {
			return fmt.Errorf("%s exists but %s does not - stale checkout?", root, meta)
		}

		_, err := os.Stat(p.Internal.Build.Root)
		gopathExisted := err == nil

		// 某些版本控制工具要求目标的父级存在。
		parent, _ := filepath.Split(root)
		if err = os.MkdirAll(parent, 0777); err != nil {
			return err
		}
		if cfg.BuildV && !gopathExisted && p.Internal.Build.Root == cfg.BuildContext.GOPATH {
			fmt.Fprintf(os.Stderr, "created GOPATH=%s; see 'go help gopath'\n", p.Internal.Build.Root)
		}

		if err = vcsCmd.Create(root, repo); err != nil {
			return err
		}
	} else {
		// 元数据目录不存在；下载增量更新。
		if err = vcsCmd.Download(root); err != nil {
			return err
		}
	}

	if cfg.BuildN {
		// 在-n中不显示标签同步；它的噪音比任何东西都大，因为我们没有运行命令，所以找不到标签。
		// 但避免打印任何内容。
		fmt.Fprintf(os.Stderr, "# cd %s; %s sync/update\n", root, vcsCmd.Cmd)
		return nil
	}

	// 选择并同步到存储库的相应版本。
	tags, err := vcsCmd.Tags(root)
	if err != nil {
		return err
	}
	vers := runtime.Version()
	if i := strings.Index(vers, " "); i >= 0 {
		vers = vers[:i]
	}
	if err := vcsCmd.TagSync(root, selectTag(vers, tags)); err != nil {
		return err
	}

	return nil
}

// selectTag返回给定版本最匹配的标记。
// Closest指当前版本之外的最新版本。
// 版本“goX”（或“goX.Y”或“goX.Y.Z”）匹配相同形式的标签。
// Version“release.rN”匹配形式为“go.rN”的标记（N是一个浮点数）。
// Version“weekly.YYYY-MM-DD”匹配像“go.weekly.YYYY-MM-DD”这样的标签。
// 
// 注意（rsc）：最终我们需要在这里决定一些逻辑。
// 目前，只有“go1”。这与go help get中的文档匹配。
func selectTag(goVersion string, tags []string) (match string) {
	for _, t := range tags {
		if t == "go1" {
			return "go1"
		}
	}
	return ""
}

// checkImportPath就像模块一样。选中ImportPath，但它禁止在路径元素中引入点
// 。这可能会导致“go get”创建。git和其他VCS 
// 目录位于我们以后可能运行VCS工具的位置。
func checkImportPath(path string) error {
	if err := module.CheckImportPath(path); err != nil {
		return err
	}
	checkElem := func(elem string) error {
		if elem[0] == '.' {
			return fmt.Errorf("malformed import path %q: leading dot in path element", path)
		}
		return nil
	}
	elemStart := 0
	for i, r := range path {
		if r == '/' {
			if err := checkElem(path[elemStart:]); err != nil {
				return err
			}
			elemStart = i + 1
		}
	}
	if err := checkElem(path[elemStart:]); err != nil {
		return err
	}
	return nil
}
